<?php
/**
 * quick_curd
 * ============================================================================
 * 版权所有 2024-2099 wltx666.com，并保留所有权利。
 * 网站地址: https://wltx666.com
 * ============================================================================
 * Author: 草上飞
 * Email: wltx@alipay168.cn
 * QQ: 2719875001
 * Date: 2024/12/15
 * 描述:
 */

namespace app\service;


class LockService
{
    private static $retryDelay = 200;//重试间隔时间，单位ms
    private static $retryCount = 5;//重试次数
    private static $clockDriftFactor = 0.01;//过期时间精度
    private static $quorum;
    private static $servers = [];//redis服务器list
    private static $instances = [];//redis连接实例

    public function __construct($servers = [])
    {
        self::$servers = $servers;
    }

    /**
     * @param $key string key名
     * @param $is_only boolean 是否唯一锁（适用于防止重复提交），默认true，true的话则不轮询抢锁，可根据业务需求是否要删除锁
     * @param $ttl integer 有效期，默认5秒
     * @return array|false
     */
    public static function logck_by_second($key, $ttl = 5, $is_only = true)
    {
        return self::lock($key, $is_only, $ttl * 1000);
    }

    /**
     * @param $key string key名
     * @param $is_only boolean 是否唯一锁（适用于防止重复提交），默认false，true的话则不轮询抢锁，可根据业务需求是否要删除锁
     * @param $ttl integer 有效期，默认5000毫秒（5秒）
     * @return array|false
     */
    public static function lock($key, $is_only = false, $ttl = 5000)
    {
        try {
            self::initInstances();
        } catch (\Exception $e) {
            bs_log($e->getMessage(), 'redis故障', 'error.log');
            return false;
        }
        $token = uniqid();
        $retry = self::$retryCount;
        do {
            $n = 0;
            $startTime = microtime(true) * 1000;
            foreach (self::$instances as $instance) {
                if (self::lockInstance($instance, $key, $token, $ttl)) {
                    $n++;
                }
            }

            // 向漂移添加2毫秒，以说明Redis过期
            // 精度，即1毫秒，加上1毫秒的最小漂移
            // 小于最小过期时间
            $drift = ($ttl * self::$clockDriftFactor) + 2;
            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
            if ($n > self::$quorum && $validityTime > 0) {
                return [
                    'validity' => $validityTime,
                    'resource' => $key,
                    'token' => $token,
                ];
            } else {
                foreach (self::$instances as $instance) {
                    self::unlockInstance($instance, $key, $token);
                }
            }
            // 等待随机延迟，然后重试
            $delay = mt_rand(floor(self::$retryDelay / 2), self::$retryDelay);
            usleep($delay * 1000);
            $retry--;
        } while ($retry > 0 && !$is_only);
        return false;
    }

    /**
     * @param array|string $lock 锁或者key名
     */
    public static function unlock($lock)
    {
        try {
            self::initInstances();
        } catch (\Exception $e) {
            return false;
        }
        if (is_string($lock)) {
            $resource = $lock;
            $token = self::$instances[0]->get($resource);
        } else {
            $resource = $lock['resource'];
            $token = $lock['token'];
        }
        foreach (self::$instances as $instance) {
            self::unlockInstance($instance, $resource, $token);
        }
    }

    /**
     * @throws \Exception
     */
    private static function initInstances($servers = []): void
    {
        if (empty($servers)) {
            if (empty(self::$servers)) {
                self::$servers[] = config('cache.stores.redis');
            }
        } else {
            self::$servers = $servers;
        }
        self::$quorum = min(count($servers), (count($servers) / 2 + 1));

        if (empty(self::$instances)) {
            if (!extension_loaded('redis')) {
                throw new \Exception('redis扩展未安装');
            }
            foreach (self::$servers as $server) {
                // 创建 Redis 实例
                $redis = new \Redis();
                $redis->connect($server['host'], $server['port'], $server['timeout'] ?? 0);
                if (!empty($server['password'])) {
                    $redis->auth($server['password']);
                }
                if (!empty($server['index'])) {
                    $redis->select($server['index']);
                }
                self::$instances[] = $redis;
            }
        }
    }

    // 加锁
    private static function lockInstance($instance, $resource, $token, $ttl)
    {
        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
    }

    // 释放锁
    private static function unlockInstance($instance, $resource, $token)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$resource, $token], 1);
    }
}
